﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.IO;
using Skugo.Shared;

namespace Skugo
{
    // A client basically just stores data
    public class Client
    {
        // Store the connection
        public readonly Connection Connection;

        // The plugin container contains the list of all local plugins
        public readonly PluginContainer LocalPlugins;

        // Store the created packet router
        public readonly PacketRouter Router;

        /// <summary>
        /// A userdata field that can be set to anything by the user.
        /// </summary>
        public Object UserData;

        /// <summary>
        /// Checks if the client is locally connected.
        /// </summary>
        public Boolean IsLocal
        {
            get { return this.Connection.IsLocal; }
        }

        // Constructor
        public Client(Connection connection)
        {
            this.Connection = connection;
            this.LocalPlugins = new PluginContainer();
            this.Router = new PacketRouter(connection, this.LocalPlugins);
        }
    };

    public delegate void ClientFn(Client client);
    public delegate void UpdateFn();

    public class Framework
    {
        /// <summary>
        /// Tries to limit all skugo plugins to a given amount of time per update.
        /// This field can be adjusted, but is meant to keep the host running smoothly.
        /// </summary>
        public Single TimeLimitPerUpdateMs = 2.0f;

        /// <summary>
        /// Whether or not we were locally run.
        /// </summary>
        public Boolean IsLocal { get; private set; }

        /// <summary>
        /// The amount of allowed time this frame (can roll over)!
        /// </summary>
        private Single CurrentAllowedTime;

        /// <summary>
        /// The default port that we'll connect to.
        /// </summary>
        public const UInt16 DefaultPort = 8000;

        /// <summary>
        /// Notify a user when a client connects.
        /// </summary>
        public event ClientFn OnClientConnected;

        /// <summary>
        /// Notify a user when a client disconnects.
        /// </summary>
        public event ClientFn OnClientDisconnected;

        /// <summary>
        /// An event that occurs every time the framework is updated.
        /// </summary>
        public event UpdateFn OnUpdate
        {
            add
            {
                this.ToBeUpdated.Add(value);
            }

            remove
            {
                this.ToBeUpdated.Remove(value);
            }
        }

        private List<UpdateFn> ToBeUpdated = new List<UpdateFn>();

        /// <summary>
        /// The singleton instance of the framework.
        /// </summary>
        public static Framework Instance { get; private set; }

        /// <summary>
        /// A random number generator for load balancing.
        /// </summary>
        private Random Rand = new Random();

        /// <summary>
        /// A host is responsible for establishing connections to the external tool.
        /// </summary>
        private Host Host = new Host();

        /// <summary>
        /// Store a list of all the clients.
        /// </summary>
        private List<Client> Clients = new List<Client>();

        /// <summary>
        /// The name of the project (typically this gets used to identify configurations).
        /// </summary>
        private String ProjectName;

        /// <summary>
        /// A privatized constructor (so that the only way to access the framework is through 'Instance').
        /// </summary>
        private Framework()
        {
        }

        /// <summary>
        /// Start hosting on the default port.
        /// </summary>
        /// <param name="projectName">A name that uniquely identifies your project.</param>
        /// <param name="parameters">The arguments passed into the process (command line arguments)</param>
        public static void Start(String projectName, String[] parameters)
        {
            Start(projectName, parameters, DefaultPort);
        }

        /// <summary>
        /// Start hosting on the given port.
        /// </summary>
        /// <param name="projectName">A name that uniquely identifies your project.</param>
        /// <param name="parameters">The arguments passed into the process (command line arguments)</param>
        /// <param name="recommendedPort">A recommended port that we host on.
        /// This can be overridden by certain command line arguments.</param>
        public static void Start(String projectName, String[] parameters, UInt16 recommendedPort)
        {
            // Error checking
            if (Instance != null)
            {
                throw new Exception("The Skugo framework was already started, and cannot be started again");
            }

            // Create the framework
            Instance = new Framework();

            // We look for a special command line parameter that tells us if we're being launched by skugo
            const String LocalParameter = "-SkugoLocal:";

            // Are we running locally? (assume not)
            Instance.IsLocal = false;

            // Loop through each of the parameters
            foreach (var parameter in parameters)
            {
                // If we found the local parameter...
                if (parameter.StartsWith(LocalParameter))
                {
                    // We are running locally!
                    Instance.IsLocal = true;

                    // The local parameter should also have a port that we're using
                    var portText = parameter.Substring(LocalParameter.Length);

                    // Try and parse the parameter port text into a number
                    Int32 port;
                    if (Int32.TryParse(portText, out port))
                    {
                        // We parsed the port!
                        recommendedPort = (UInt16)port;
                    }
                }
            }

            // Finally, set the project name and start hosting
            Instance.ProjectName = projectName;
            Instance.Host.StartHosting(recommendedPort);
        }

        /// <summary>
        /// Stop the framework (any cut off any active connections).
        /// </summary>
        public void Stop()
        {
            // Shutdown the host, which kills all client connections
            this.Host.Shutdown();

            // Update one last time so that all clients will be properly disposed of
            // The connection for each client will be tested, and it will cause the clients to be removed
            this.Update();

            // Clear the instance of the framework
            Instance = null;
        }

        /// <summary>
        /// Updating the framework will pump all incoming messages (this is required!).
        /// </summary>
        public void Update()
        {
            // Create a stopwatch to balance out time
            Stopwatch timeLeft = new Stopwatch();
            timeLeft.Start();

            // Clone the list of event handlers
            var toBeUpdated = this.ToBeUpdated.ToList();

            // Add to the allowed amount of time
            this.CurrentAllowedTime += this.TimeLimitPerUpdateMs;

            // Never go over the allowed amount (we roll over negatives, not positives!)
            if (this.CurrentAllowedTime > this.TimeLimitPerUpdateMs)
            {
                this.CurrentAllowedTime = this.TimeLimitPerUpdateMs;
            }

            // While we still have time...
            while (toBeUpdated.Count != 0)
            {
                // Get the time ran in milliseconds
                var timeMs = (Single)timeLeft.Elapsed.TotalMilliseconds;

                // If we go outside the time limit...
                if (timeMs > this.CurrentAllowedTime)
                {
                    // Stop here!
                    timeLeft.Stop();
                    break;
                }

                // Generate an index for an update function
                Int32 index = this.Rand.Next(toBeUpdated.Count);

                // Grab a random update callback
                var update = toBeUpdated[index];

                // Swap-erase the index
                Int32 lastIndex = toBeUpdated.Count - 1;
                toBeUpdated[index] = toBeUpdated[lastIndex];
                toBeUpdated.RemoveAt(lastIndex);

                // Invoke the update event
                update();
            }

            // Subtract off the amount of time spent
            this.CurrentAllowedTime -= (Single)timeLeft.Elapsed.TotalMilliseconds;

            // Store each connection that we pop from the host
            Connection newConnection = null;

            // Attempt to pop a new connection from the host
            while ((newConnection = this.Host.PopNewConnection()) != null)
            {
                // Create a new client
                var newClient = new Client(newConnection);

                // Create a client and add it to the list
                this.Clients.Add(newClient);

                // Tell the client the project name
                this.SendProjectName(newClient);

                // If anyone is listening to the event
                if (this.OnClientConnected != null)
                {
                    // Send an event that a client joined
                    this.OnClientConnected.Invoke(newClient);
                }
            }

            // Loop through all the clients and process incoming packets
            for (Int32 i = 0; i < this.Clients.Count;)
            {
                // Get the current client
                var client = this.Clients[i];

                // If the client ever disconnects...
                if (client.Connection.IsConnected == false)
                {
                    // If anyone is listening to the event
                    if (this.OnClientDisconnected != null)
                    {
                        // Send an event that a client disconnected
                        this.OnClientDisconnected.Invoke(client);
                    }

                    // Destroy all of the plugins for that client
                    client.LocalPlugins.DestroyAllPlugins();

                    // Remove the current client (which will shift everything in front)
                    this.Clients.RemoveAt(i);

                    // If we were running locally, and the client that just disconnected was our local client...
                    if (this.IsLocal && client.IsLocal)
                    {
                        // Kill the current process
                        var thisProcess = Process.GetCurrentProcess();
                        thisProcess.Kill();
                        return;
                    }
                }
                else
                {
                    // Process all the incoming packets to this client
                    client.Router.ProcessAllIncoming();

                    // Increment to the next client
                    ++i;
                }
            }
        }

        /// <summary>
        /// Send the name of the project to the current client.
        /// </summary>
        /// <param name="client">The client that we want to send the name to.</param>
        private void SendProjectName(Client client)
        {
            // Create a memory stream to write data to
            MemoryStream stream = new MemoryStream();

            // Create a binary writer to write the data
            BinaryWriter outgoing = new BinaryWriter(stream);

            // Write out the packet data
            outgoing.Write((UInt32)(sizeof(UInt32) + this.ProjectName.Length));
            outgoing.Write((Byte)PacketType.Initialize);

            // All of our packets currently require an instance guid
            // This packet type does not, but I'd rather not change it everywhere
            // I'm basically writing out a dummy instance guid
            outgoing.Write((Int64)(-1));

            // Write the provided data
            outgoing.WriteAsciiString(this.ProjectName);

            // Send the packet out
            client.Connection.Write(stream.ToArray());
        }

        /// <summary>
        /// Log a line of text (no newline required).
        /// </summary>
        /// <param name="text">The text to be logged.</param>
        public static void Log(String text)
        {
            Debug.WriteLine(text);
        }
    }
}
